/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package com.googlecode.sweetened.typedef;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.Date;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.util.DOMElementWriter;
import org.apache.tools.ant.util.XMLFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import com.googlecode.sweetened.SweetenedClasspathTask;


/**
 * Represents the xml fragment within a SweetenedClasspath
 */
public class SweetenedXML extends XMLFragment {

    public void execute(String data, File file, boolean autoGen, boolean debug) {

        SweetenedElementWriter writer = new SweetenedElementWriter(true, NamespacePolicy.DEFAULT.getPolicy());
        writer.setData(data);
        OutputStream os = new ByteArrayOutputStream();
        try {
            Node n = getFragment().getFirstChild();
            if (n == null) {
                throw new BuildException("No nested XML specified");
            }
            writer.write((Element) n, os);
        } catch (BuildException e) {
            throw e;
        } catch (Exception e) {
            throw new BuildException(e);
        }

        String results = os.toString();
        if (autoGen) {
            String note = "\n<!-- Generated by Sweetened http://sweetened.googlecode.com/ (" + new Date() + ") -->";
            results = results.replace("\"?>", "\"?>" + note);
        }
        if (debug)
            log(results);

        FileOutputStream fos;
        try {
            fos = new FileOutputStream(file);
            fos.write(results.getBytes("UTF-8"));
            fos.close();
        } catch (FileNotFoundException ex) {
            throw new BuildException(ex);
        } catch (UnsupportedEncodingException ex) {
            throw new BuildException(ex);
        } catch (IOException ex) {
            throw new BuildException(ex);
        }
    }

    public static class NamespacePolicy extends EnumeratedAttribute {
        private static final String IGNORE = "ignore";
        private static final String ELEMENTS = "elementsOnly";
        private static final String ALL = "all";

        public static final NamespacePolicy DEFAULT
            = new NamespacePolicy(IGNORE);

        public NamespacePolicy() {}

        public NamespacePolicy(String s) {
            setValue(s);
        }
        /** {@inheritDoc}. */
        public String[] getValues() {
            return new String[] {IGNORE, ELEMENTS, ALL};
        }

        public DOMElementWriter.XmlNamespacePolicy getPolicy() {
            String s = getValue();
            if (IGNORE.equalsIgnoreCase(s)) {
                return DOMElementWriter.XmlNamespacePolicy.IGNORE;
            } else if (ELEMENTS.equalsIgnoreCase(s)) {
                return
                    DOMElementWriter.XmlNamespacePolicy.ONLY_QUALIFY_ELEMENTS;
            } else if (ALL.equalsIgnoreCase(s)) {
                return DOMElementWriter.XmlNamespacePolicy.QUALIFY_ALL;
            } else {
                throw new BuildException("Invalid namespace policy: " + s);
            }
        }
    }

    /**
     * Helper class that knows how to write out the sweetenedEntries.
     */
    private class SweetenedElementWriter extends DOMElementWriter {
        private String lSep = System.getProperty("line.separator");
        private String data = null;

        public SweetenedElementWriter(boolean xmlDeclaration, XmlNamespacePolicy namespacePolicy) {
            super(xmlDeclaration, namespacePolicy);
        }

        /**
         * Writes a DOM tree to a stream.
         *
         * @param element the Root DOM element of the tree
         * @param out where to send the output
         * @param indent number of
         * @param indentWith string that should be used to indent the
         * corresponding tag.
         * @throws IOException if an error happens while writing to the stream.
         */
        public void write(Element element, Writer out, int indent,
                          String indentWith)
            throws IOException {

            // Write child elements and text
            NodeList children = element.getChildNodes();
            boolean hasChildren = (children.getLength() > 0);
            boolean hasChildElements = false;
            openElement(element, out, indent, indentWith, hasChildren);

            if (hasChildren) {
                for (int i = 0; i < children.getLength(); i++) {
                    Node child = children.item(i);

                    switch (child.getNodeType()) {

                    case Node.ELEMENT_NODE:
                        hasChildElements = true;
                        if (i == 0) {
                            out.write(lSep);
                        }
                        if (child.getNodeName().equals(SweetenedClasspathTask.SWEETENEDENTRIES_ELEMENT)) {
                            out.write(data);
                        } else {
                            write((Element) child, out, indent + 1, indentWith);
                        }
                        break;

                    case Node.TEXT_NODE:
                        out.write(encode(child.getNodeValue()));
                        break;

                    case Node.COMMENT_NODE:
                        out.write("<!--");
                        out.write(encode(child.getNodeValue()));
                        out.write("-->");
                        break;

                    case Node.CDATA_SECTION_NODE:
                        out.write("<![CDATA[");
                        out.write(encodedata(((Text) child).getData()));
                        out.write("]]>");
                        break;

                    case Node.ENTITY_REFERENCE_NODE:
                        out.write('&');
                        out.write(child.getNodeName());
                        out.write(';');
                        break;

                    case Node.PROCESSING_INSTRUCTION_NODE:
                        out.write("<?");
                        out.write(child.getNodeName());
                        String data = child.getNodeValue();
                        if (data != null && data.length() > 0) {
                            out.write(' ');
                            out.write(data);
                        }
                        out.write("?>");
                        break;
                    default:
                        // Do nothing
                    }
                }
                closeElement(element, out, indent, indentWith, hasChildElements);
            }
        }

        /**
         * Had to override this method and add support for & # 10;
         * Not a great solution... but this works...
         * NOTE: this is fixed in ant > 1.8.1.
         */
        public String encode(String value) {
            StringBuffer sb = new StringBuffer();
            int len = value.length();
            for (int i = 0; i < len; i++) {
                char c = value.charAt(i);
                switch (c) {
                case '<':
                    sb.append("&lt;");
                    break;
                case '>':
                    sb.append("&gt;");
                    break;
                case '\'':
                    sb.append("&apos;");
                    break;
                case '\"':
                    sb.append("&quot;");
                    break;
                case '\n':
                    sb.append("&#10;");
                    break;
                case '&':
                    int nextSemi = value.indexOf(";", i);
                    if (nextSemi < 0
                        || !isReference(value.substring(i, nextSemi + 1))) {
                        sb.append("&amp;");
                    } else {
                        sb.append('&');
                    }
                    break;
                default:
                    if (isLegalCharacter(c)) {
                        sb.append(c);
                    }
                    break;
                }
            }
            return sb.substring(0);
        }

        public void setData(String data) {
            this.data = data;
        }
    }
}
